// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "net/proxy/proxy_config_service_mac.h"

#include <CoreFoundation/CoreFoundation.h>
#include <SystemConfiguration/SystemConfiguration.h>

#include "base/bind.h"
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/sys_string_conversions.h"
#include "net/base/net_errors.h"
#include "net/proxy/proxy_config.h"
#include "net/proxy/proxy_info.h"
#include "net/proxy/proxy_server.h"

namespace net {

namespace {

    // Utility function to pull out a boolean value from a dictionary and return it,
    // returning a default value if the key is not present.
    bool GetBoolFromDictionary(CFDictionaryRef dict,
        CFStringRef key,
        bool default_value)
    {
        CFNumberRef number = base::mac::GetValueFromDictionary<CFNumberRef>(dict,
            key);
        if (!number)
            return default_value;

        int int_value;
        if (CFNumberGetValue(number, kCFNumberIntType, &int_value))
            return int_value;
        else
            return default_value;
    }

    void GetCurrentProxyConfig(ProxyConfig* config)
    {
        base::ScopedCFTypeRef<CFDictionaryRef> config_dict(
            SCDynamicStoreCopyProxies(NULL));
        DCHECK(config_dict);

        // auto-detect

        // There appears to be no UI for this configuration option, and we're not sure
        // if Apple's proxy code even takes it into account. But the constant is in
        // the header file so we'll use it.
        config->set_auto_detect(
            GetBoolFromDictionary(config_dict.get(),
                kSCPropNetProxiesProxyAutoDiscoveryEnable,
                false));

        // PAC file

        if (GetBoolFromDictionary(config_dict.get(),
                kSCPropNetProxiesProxyAutoConfigEnable,
                false)) {
            CFStringRef pac_url_ref = base::mac::GetValueFromDictionary<CFStringRef>(
                config_dict.get(), kSCPropNetProxiesProxyAutoConfigURLString);
            if (pac_url_ref)
                config->set_pac_url(GURL(base::SysCFStringRefToUTF8(pac_url_ref)));
        }

        // proxies (for now ftp, http, https, and SOCKS)

        if (GetBoolFromDictionary(config_dict.get(),
                kSCPropNetProxiesFTPEnable,
                false)) {
            ProxyServer proxy_server = ProxyServer::FromDictionary(ProxyServer::SCHEME_HTTP,
                config_dict.get(),
                kSCPropNetProxiesFTPProxy,
                kSCPropNetProxiesFTPPort);
            if (proxy_server.is_valid()) {
                config->proxy_rules().type = ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME;
                config->proxy_rules().proxies_for_ftp.SetSingleProxyServer(proxy_server);
            }
        }
        if (GetBoolFromDictionary(config_dict.get(),
                kSCPropNetProxiesHTTPEnable,
                false)) {
            ProxyServer proxy_server = ProxyServer::FromDictionary(ProxyServer::SCHEME_HTTP,
                config_dict.get(),
                kSCPropNetProxiesHTTPProxy,
                kSCPropNetProxiesHTTPPort);
            if (proxy_server.is_valid()) {
                config->proxy_rules().type = ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME;
                config->proxy_rules().proxies_for_http.SetSingleProxyServer(proxy_server);
            }
        }
        if (GetBoolFromDictionary(config_dict.get(),
                kSCPropNetProxiesHTTPSEnable,
                false)) {
            ProxyServer proxy_server = ProxyServer::FromDictionary(ProxyServer::SCHEME_HTTP,
                config_dict.get(),
                kSCPropNetProxiesHTTPSProxy,
                kSCPropNetProxiesHTTPSPort);
            if (proxy_server.is_valid()) {
                config->proxy_rules().type = ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME;
                config->proxy_rules().proxies_for_https.SetSingleProxyServer(proxy_server);
            }
        }
        if (GetBoolFromDictionary(config_dict.get(),
                kSCPropNetProxiesSOCKSEnable,
                false)) {
            ProxyServer proxy_server = ProxyServer::FromDictionary(ProxyServer::SCHEME_SOCKS5,
                config_dict.get(),
                kSCPropNetProxiesSOCKSProxy,
                kSCPropNetProxiesSOCKSPort);
            if (proxy_server.is_valid()) {
                config->proxy_rules().type = ProxyConfig::ProxyRules::TYPE_PROXY_PER_SCHEME;
                config->proxy_rules().fallback_proxies.SetSingleProxyServer(proxy_server);
            }
        }

        // proxy bypass list

        CFArrayRef bypass_array_ref = base::mac::GetValueFromDictionary<CFArrayRef>(
            config_dict.get(), kSCPropNetProxiesExceptionsList);
        if (bypass_array_ref) {
            CFIndex bypass_array_count = CFArrayGetCount(bypass_array_ref);
            for (CFIndex i = 0; i < bypass_array_count; ++i) {
                CFStringRef bypass_item_ref = base::mac::CFCast<CFStringRef>(
                    CFArrayGetValueAtIndex(bypass_array_ref, i));
                if (!bypass_item_ref) {
                    LOG(WARNING) << "Expected value for item " << i
                                 << " in the kSCPropNetProxiesExceptionsList"
                                    " to be a CFStringRef but it was not";

                } else {
                    config->proxy_rules().bypass_rules.AddRuleFromString(
                        base::SysCFStringRefToUTF8(bypass_item_ref));
                }
            }
        }

        // proxy bypass boolean

        if (GetBoolFromDictionary(config_dict.get(),
                kSCPropNetProxiesExcludeSimpleHostnames,
                false)) {
            config->proxy_rules().bypass_rules.AddRuleToBypassLocal();
        }

        // Source
        config->set_source(PROXY_CONFIG_SOURCE_SYSTEM);
    }

} // namespace

// Reference-counted helper for posting a task to
// ProxyConfigServiceMac::OnProxyConfigChanged between the notifier and IO
// thread. This helper object may outlive the ProxyConfigServiceMac.
class ProxyConfigServiceMac::Helper
    : public base::RefCountedThreadSafe<ProxyConfigServiceMac::Helper> {
public:
    explicit Helper(ProxyConfigServiceMac* parent)
        : parent_(parent)
    {
        DCHECK(parent);
    }

    // Called when the parent is destroyed.
    void Orphan()
    {
        parent_ = NULL;
    }

    void OnProxyConfigChanged(const ProxyConfig& new_config)
    {
        if (parent_)
            parent_->OnProxyConfigChanged(new_config);
    }

private:
    friend class base::RefCountedThreadSafe<Helper>;
    ~Helper() { }

    ProxyConfigServiceMac* parent_;
};

void ProxyConfigServiceMac::Forwarder::SetDynamicStoreNotificationKeys(
    SCDynamicStoreRef store)
{
    proxy_config_service_->SetDynamicStoreNotificationKeys(store);
}

void ProxyConfigServiceMac::Forwarder::OnNetworkConfigChange(
    CFArrayRef changed_keys)
{
    proxy_config_service_->OnNetworkConfigChange(changed_keys);
}

ProxyConfigServiceMac::ProxyConfigServiceMac(
    const scoped_refptr<base::SingleThreadTaskRunner>& io_thread_task_runner)
    : forwarder_(this)
    , has_fetched_config_(false)
    , helper_(new Helper(this))
    , io_thread_task_runner_(io_thread_task_runner)
{
    DCHECK(io_thread_task_runner_.get());
    config_watcher_.reset(new NetworkConfigWatcherMac(&forwarder_));
}

ProxyConfigServiceMac::~ProxyConfigServiceMac()
{
    DCHECK(io_thread_task_runner_->BelongsToCurrentThread());
    // Delete the config_watcher_ to ensure the notifier thread finishes before
    // this object is destroyed.
    config_watcher_.reset();
    helper_->Orphan();
}

void ProxyConfigServiceMac::AddObserver(Observer* observer)
{
    DCHECK(io_thread_task_runner_->BelongsToCurrentThread());
    observers_.AddObserver(observer);
}

void ProxyConfigServiceMac::RemoveObserver(Observer* observer)
{
    DCHECK(io_thread_task_runner_->BelongsToCurrentThread());
    observers_.RemoveObserver(observer);
}

ProxyConfigService::ConfigAvailability
ProxyConfigServiceMac::GetLatestProxyConfig(ProxyConfig* config)
{
    DCHECK(io_thread_task_runner_->BelongsToCurrentThread());

    // Lazy-initialize by fetching the proxy setting from this thread.
    if (!has_fetched_config_) {
        GetCurrentProxyConfig(&last_config_fetched_);
        has_fetched_config_ = true;
    }

    *config = last_config_fetched_;
    return has_fetched_config_ ? CONFIG_VALID : CONFIG_PENDING;
}

void ProxyConfigServiceMac::SetDynamicStoreNotificationKeys(
    SCDynamicStoreRef store)
{
    // Called on notifier thread.

    CFStringRef proxies_key = SCDynamicStoreKeyCreateProxies(NULL);
    CFArrayRef key_array = CFArrayCreate(
        NULL, (const void**)(&proxies_key), 1, &kCFTypeArrayCallBacks);

    bool ret = SCDynamicStoreSetNotificationKeys(store, key_array, NULL);
    // TODO(willchan): Figure out a proper way to handle this rather than crash.
    CHECK(ret);

    CFRelease(key_array);
    CFRelease(proxies_key);
}

void ProxyConfigServiceMac::OnNetworkConfigChange(CFArrayRef changed_keys)
{
    // Called on notifier thread.

    // Fetch the new system proxy configuration.
    ProxyConfig new_config;
    GetCurrentProxyConfig(&new_config);

    // Call OnProxyConfigChanged() on the IO thread to notify our observers.
    io_thread_task_runner_->PostTask(
        FROM_HERE,
        base::Bind(&Helper::OnProxyConfigChanged, helper_.get(), new_config));
}

void ProxyConfigServiceMac::OnProxyConfigChanged(
    const ProxyConfig& new_config)
{
    DCHECK(io_thread_task_runner_->BelongsToCurrentThread());

    // Keep track of the last value we have seen.
    has_fetched_config_ = true;
    last_config_fetched_ = new_config;

    // Notify all the observers.
    FOR_EACH_OBSERVER(Observer, observers_,
        OnProxyConfigChanged(new_config, CONFIG_VALID));
}

} // namespace net
